v{{ swh_web_version }}
v{{ swh_web_version|split:"+"|first }}
diff --git a/assets/src/bundles/add_forge/create-request.js b/assets/src/bundles/add_forge/create-request.js
index 3d23afb0..babce755 100644
--- a/assets/src/bundles/add_forge/create-request.js
+++ b/assets/src/bundles/add_forge/create-request.js
@@ -1,155 +1,128 @@
/**
* Copyright (C) 2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
-import {handleFetchError, removeUrlFragment, csrfPost,
+import {handleFetchError, csrfPost,
getHumanReadableDate} from 'utils/functions';
import userRequestsFilterCheckboxFn from 'utils/requests-filter-checkbox.ejs';
import {swhSpinnerSrc} from 'utils/constants';
let requestBrowseTable;
const addForgeCheckboxId = 'swh-add-forge-user-filter';
const userRequestsFilterCheckbox = userRequestsFilterCheckboxFn({
'inputId': addForgeCheckboxId,
'checked': true // by default, display only user requests
});
export function onCreateRequestPageLoad() {
$(document).ready(() => {
$('#requestCreateForm').submit(async function(event) {
event.preventDefault();
try {
const response = await csrfPost($(this).attr('action'),
{'Content-Type': 'application/x-www-form-urlencoded'},
$(this).serialize());
handleFetchError(response);
$('#userMessageDetail').empty();
$('#userMessage').text('Your request has been submitted');
$('#userMessage').removeClass('badge-danger');
$('#userMessage').addClass('badge-success');
requestBrowseTable.draw(); // redraw the table to update the list
} catch (errorResponse) {
$('#userMessageDetail').empty();
let errorMessage;
let errorMessageDetail = '';
const errorData = await errorResponse.json();
// if (errorResponse.content_type === 'text/plain') { // does not work?
if (errorResponse.status === 409) {
errorMessage = errorData;
} else { // assuming json response
// const exception = errorData['exception'];
errorMessage = 'An unknown error occurred during the request creation';
try {
const reason = JSON.parse(errorData['reason']);
Object.entries(reason).forEach((keys, _) => {
const key = keys[0];
const message = keys[1][0]; // take only the first issue
errorMessageDetail += `\n${key}: ${message}`;
});
} catch (_) {
errorMessageDetail = errorData['reason']; // can't parse it, leave it raw
}
}
$('#userMessage').text(
errorMessageDetail ? `Error: ${errorMessageDetail}` : errorMessage
);
$('#userMessage').removeClass('badge-success');
$('#userMessage').addClass('badge-danger');
}
});
- $('#swh-add-forge-requests-list-tab').on('shown.bs.tab', () => {
- requestBrowseTable.draw();
- window.location.hash = '#browse-requests';
- });
-
- $('#swh-add-forge-requests-help-tab').on('shown.bs.tab', () => {
- window.location.hash = '#help';
- });
-
- $('#swh-add-forge-tab').on('shown.bs.tab', () => {
- removeUrlFragment();
- });
-
- $(window).on('hashchange', () => {
- onPageHashChage();
- });
- onPageHashChage(); // Explicit call to handle a hash during the page load
populateRequestBrowseList(); // Load existing requests
});
}
-function onPageHashChage() {
- if (window.location.hash === '#browse-requests') {
- $('.nav-tabs a[href="#swh-add-forge-requests-list"]').tab('show');
- } else if (window.location.hash === '#help') {
- $('.nav-tabs a[href="#swh-add-forge-requests-help"]').tab('show');
- } else {
- $('.nav-tabs a[href="#swh-add-forge-submit-request"]').tab('show');
- }
-}
-
export function populateRequestBrowseList() {
requestBrowseTable = $('#add-forge-request-browse')
.on('error.dt', (e, settings, techNote, message) => {
$('#add-forge-browse-request-error').text(message);
})
.DataTable({
serverSide: true,
processing: true,
language: {
processing: ``
},
retrieve: true,
searching: true,
info: false,
// Layout configuration, see [1] for more details
// [1] https://datatables.net/reference/option/dom
dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' +
'<"row"<"col-sm-12"tr>>' +
'<"row"<"col-sm-5"i><"col-sm-7"p>>',
ajax: {
'url': Urls.add_forge_request_list_datatables(),
data: (d) => {
if (swh.webapp.isUserLoggedIn() && $(`#${addForgeCheckboxId}`).prop('checked')) {
d.user_requests_only = '1';
}
}
},
fnInitComplete: function() {
if (swh.webapp.isUserLoggedIn()) {
$('div.user-requests-filter').html(userRequestsFilterCheckbox);
$(`#${addForgeCheckboxId}`).on('change', () => {
requestBrowseTable.draw();
});
}
},
columns: [
{
data: 'submission_date',
name: 'submission_date',
render: getHumanReadableDate
},
{
data: 'forge_type',
name: 'forge_type'
},
{
data: 'forge_url',
name: 'forge_url'
},
{
data: 'status',
name: 'status',
render: function(data, type, row, meta) {
return swh.add_forge.formatRequestStatusName(data);
}
}
]
});
}
diff --git a/cypress/integration/add-forge-now-request-create.spec.js b/cypress/integration/add-forge-now-request-create.spec.js
index d9612f4e..22779994 100644
--- a/cypress/integration/add-forge-now-request-create.spec.js
+++ b/cypress/integration/add-forge-now-request-create.spec.js
@@ -1,255 +1,262 @@
/**
* Copyright (C) 2022 The Software Heritage developers
* See the AUTHORS file at the top-level directory of this distribution
* License: GNU Affero General Public License version 3, or any later version
* See top-level LICENSE file for more information
*/
function populateForm(type, url, contact, email, consent, comment) {
cy.get('#swh-input-forge-type').select(type);
cy.get('#swh-input-forge-url').clear().type(url, {delay: 0, force: true});
cy.get('#swh-input-forge-contact-name').clear().type(contact, {delay: 0, force: true});
cy.get('#swh-input-forge-contact-email').clear().type(email, {delay: 0, force: true});
if (comment) {
cy.get('#swh-input-forge-comment').clear().type(comment, {delay: 0, force: true});
}
cy.get('#swh-input-consent-check').click({force: consent === 'on'});
}
describe('Browse requests list tests', function() {
beforeEach(function() {
- this.addForgeNowUrl = this.Urls.forge_add();
+ this.addForgeNowUrl = this.Urls.forge_add_create();
this.listAddForgeRequestsUrl = this.Urls.add_forge_request_list_datatables();
});
it('should not show user requests filter checkbox for anonymous users', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#swh-add-forge-user-filter').should('not.exist');
});
it('should show user requests filter checkbox for authenticated users', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
});
it('should only display user requests when filter is activated', function() {
// Clean up previous state
cy.task('db:add_forge_now:delete');
// 'user2' logs in and create requests
cy.user2Login();
cy.visit(this.addForgeNowUrl);
// create requests for the user 'user'
populateForm('gitlab', 'gitlab.org', 'admin', 'admin@example.org', 'on', '');
cy.get('#requestCreateForm').submit();
// user requests filter checkbox should be in the DOM
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
// check unfiltered user requests
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(1);
});
// user1 logout
cy.contains('a', 'logout').click();
// user logs in
cy.userLogin();
cy.visit(this.addForgeNowUrl);
populateForm('gitea', 'gitea.org', 'admin', 'admin@example.org', 'on', '');
cy.get('#requestCreateForm').submit();
populateForm('cgit', 'cgit.org', 'admin', 'admin@example.org', 'on', '');
cy.get('#requestCreateForm').submit();
// user requests filter checkbox should be in the DOM
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked');
+ // Uncheck and re-check again, to synchronize table state with the checkbox
+ // FIXME: this should not be needed
+ cy.get('#swh-add-forge-user-filter').click().click();
+
// check unfiltered user requests
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(2);
});
cy.get('#swh-add-forge-user-filter')
.uncheck({force: true});
// Users now sees everything
cy.get('tbody tr').then(rows => {
expect(rows.length).to.eq(2 + 1);
});
});
});
describe('Test add-forge-request creation', function() {
beforeEach(function() {
- this.addForgeNowUrl = this.Urls.forge_add();
+ this.addForgeNowUrl = this.Urls.forge_add_create();
});
it('should show all the tabs for every user', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-tab')
.should('have.class', 'nav-link');
cy.get('#swh-add-forge-requests-list-tab')
.should('have.class', 'nav-link');
cy.get('#swh-add-forge-requests-help-tab')
.should('have.class', 'nav-link');
});
it('should show create forge tab by default', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
});
it('should show login link for anonymous user', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#loginLink')
.should('be.visible')
.should('contain', 'log in');
});
it('should bring back after login', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#loginLink')
.should('have.attr', 'href')
- .and('include', `${this.Urls.login()}?next=${this.Urls.forge_add()}`);
+ .and('include', `${this.Urls.login()}?next=${this.Urls.forge_add_create()}`);
});
it('should change tabs on click', function() {
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#swh-add-forge-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('not.have.class', 'active');
- cy.hash().should('eq', '#browse-requests');
+ cy.url()
+ .should('include', `${this.Urls.forge_add_list()}`);
cy.get('#swh-add-forge-requests-help-tab').click();
cy.get('#swh-add-forge-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('have.class', 'active');
- cy.hash().should('eq', '#help');
+ cy.url()
+ .should('include', `${this.Urls.forge_add_help()}`);
cy.get('#swh-add-forge-tab').click();
cy.get('#swh-add-forge-tab')
.should('have.class', 'active');
cy.get('#swh-add-forge-requests-list-tab')
.should('not.have.class', 'active');
cy.get('#swh-add-forge-requests-help-tab')
.should('not.have.class', 'active');
- cy.hash().should('eq', '');
+ cy.url()
+ .should('include', `${this.Urls.forge_add_create()}`);
});
it('should show create form elements to authenticated user', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
cy.get('#swh-input-forge-type')
.should('be.visible');
cy.get('#swh-input-forge-url')
.should('be.visible');
cy.get('#swh-input-forge-contact-name')
.should('be.visible');
cy.get('#swh-input-consent-check')
.should('be.visible');
cy.get('#swh-input-forge-comment')
.should('be.visible');
cy.get('#swh-input-form-submit')
.should('be.visible');
});
it('should show browse requests table for every user', function() {
// testing only for anonymous
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#add-forge-request-browse')
.should('be.visible');
cy.get('#loginLink')
- .should('not.be.visible');
+ .should('not.exist');
});
it('should update browse list on successful submission', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment');
cy.get('#requestCreateForm').submit();
cy.visit(this.addForgeNowUrl);
cy.get('#swh-add-forge-requests-list-tab').click();
cy.get('#add-forge-request-browse')
.should('be.visible')
.should('contain', 'gitlab.com');
cy.get('#add-forge-request-browse')
.should('be.visible')
.should('contain', 'Pending');
});
it('should show error message on conflict', function() {
cy.userLogin();
cy.visit(this.addForgeNowUrl);
populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment');
cy.get('#requestCreateForm').submit();
cy.get('#requestCreateForm').submit(); // Submitting the same data again
cy.get('#userMessage')
.should('have.class', 'badge-danger')
.should('contain', 'already exists');
});
it('should show error message', function() {
cy.userLogin();
cy.intercept('POST', `${this.Urls.api_1_add_forge_request_create()}**`,
{
body: {
'exception': 'BadInputExc',
'reason': '{"add-forge-comment": ["This field is required"]}'
},
statusCode: 400
}).as('errorRequest');
cy.visit(this.addForgeNowUrl);
populateForm(
'bitbucket', 'gitlab.com', 'test', 'test@example.com', 'off', 'comment'
);
cy.get('#requestCreateForm').submit();
cy.wait('@errorRequest').then((xhr) => {
cy.get('#userMessage')
.should('have.class', 'badge-danger')
.should('contain', 'field is required');
});
});
});
diff --git a/swh/web/add_forge_now/views.py b/swh/web/add_forge_now/views.py
index 95715140..1d9a2f07 100644
--- a/swh/web/add_forge_now/views.py
+++ b/swh/web/add_forge_now/views.py
@@ -1,107 +1,127 @@
# Copyright (C) 2022 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
from typing import Any, Dict, List
from django.conf.urls import url
from django.core.paginator import Paginator
from django.db.models import Q
from django.http.request import HttpRequest
from django.http.response import HttpResponse, JsonResponse
from django.shortcuts import render
from swh.web.add_forge_now.models import Request as AddForgeRequest
from swh.web.api.views.add_forge_now import (
AddForgeNowRequestPublicSerializer,
AddForgeNowRequestSerializer,
)
from swh.web.common.utils import has_add_forge_now_permission
def add_forge_request_list_datatables(request: HttpRequest) -> HttpResponse:
"""Dedicated endpoint used by datatables to display the add-forge
requests in the Web UI.
"""
draw = int(request.GET.get("draw", 0))
add_forge_requests = AddForgeRequest.objects.all()
table_data: Dict[str, Any] = {
"recordsTotal": add_forge_requests.count(),
"draw": draw,
}
search_value = request.GET.get("search[value]")
column_order = request.GET.get("order[0][column]")
field_order = request.GET.get(f"columns[{column_order}][name]", "id")
order_dir = request.GET.get("order[0][dir]", "desc")
if field_order:
if order_dir == "desc":
field_order = "-" + field_order
add_forge_requests = add_forge_requests.order_by(field_order)
per_page = int(request.GET.get("length", 10))
page_num = int(request.GET.get("start", 0)) // per_page + 1
if search_value:
add_forge_requests = add_forge_requests.filter(
Q(forge_type__icontains=search_value)
| Q(forge_url__icontains=search_value)
| Q(status__icontains=search_value)
)
if (
int(request.GET.get("user_requests_only", "0"))
and request.user.is_authenticated
):
add_forge_requests = add_forge_requests.filter(
submitter_name=request.user.username
)
paginator = Paginator(add_forge_requests, per_page)
page = paginator.page(page_num)
if has_add_forge_now_permission(request.user):
requests = AddForgeNowRequestSerializer(page.object_list, many=True).data
else:
requests = AddForgeNowRequestPublicSerializer(page.object_list, many=True).data
results = [dict(request) for request in requests]
table_data["recordsFiltered"] = add_forge_requests.count()
table_data["data"] = results
return JsonResponse(table_data)
FORGE_TYPES: List[str] = [
"bitbucket",
"cgit",
"gitlab",
"gitea",
"heptapod",
]
-def create_request(request):
+def create_request_create(request):
"""View to create a new 'add_forge_now' request.
"""
return render(
- request, "add_forge_now/create-request.html", {"forge_types": FORGE_TYPES},
+ request,
+ "add_forge_now/create-request-create.html",
+ {"forge_types": FORGE_TYPES},
)
+def create_request_list(request):
+ """View to list existing 'add_forge_now' requests.
+
+ """
+
+ return render(request, "add_forge_now/create-request-list.html",)
+
+
+def create_request_help(request):
+ """View to explain 'add_forge_now'.
+
+ """
+
+ return render(request, "add_forge_now/create-request-help.html",)
+
+
urlpatterns = [
url(
r"^add-forge/request/list/datatables/$",
add_forge_request_list_datatables,
name="add-forge-request-list-datatables",
),
- url(r"^add-forge/request/create/$", create_request, name="forge-add"),
+ url(r"^add-forge/request/create/$", create_request_create, name="forge-add-create"),
+ url(r"^add-forge/request/list/$", create_request_list, name="forge-add-list"),
+ url(r"^add-forge/request/help/$", create_request_help, name="forge-add-help"),
]
diff --git a/swh/web/templates/add_forge_now/create-request-create.html b/swh/web/templates/add_forge_now/create-request-create.html
new file mode 100644
index 00000000..661b81b5
--- /dev/null
+++ b/swh/web/templates/add_forge_now/create-request-create.html
@@ -0,0 +1,116 @@
+{% extends "./create-request.html" %}
+
+{% comment %}
+Copyright (C) 2022 The Software Heritage developers
+See the AUTHORS file at the top-level directory of this distribution
+License: GNU Affero General Public License version 3, or any later version
+See top-level LICENSE file for more information
+{% endcomment %}
+
+{% block tab_content %}
+
+ You must be logged in to submit an add forge request. Please + log in +
+ + {% else %} + + ++ Once an add-forge-request is submitted, its status can be viewed in + the + submitted requests list. This process involves a moderator approval and + might take a few days to handle (it primarily depends on the response + time from the forge). +
+ {% endif %} ++ For submitting an "Add forge now" request", you have to provide the following details: +
++ Once submitted, your "add forge" request can be in one + of the following states +
+Submission date | +Forge type | +Forge URL | +Status | +
---|
“Add forge now” provides a service for Software Heritage users to save a complete forge in the Software Heritage archive by requesting the addition of the forge URL into the list of regularly visited forges. {% if not user.is_authenticated %}
You can submit an “Add forge now” request only when you are authenticated, please login to submit the request.
{% endif %}- You must be logged in to submit an add forge request. Please - log in -
- - {% else %} - - -- Once an add-forge-request is submitted, its status can be viewed in - the - submitted requests list. This process involves a moderator approval and - might take a few days to handle (it primarily depends on the response - time from the forge). -
- {% endif %} -Submission date | -Forge type | -Forge URL | -Status | -
---|
- For submitting an "Add forge now" request", you have to provide the following details: -
-- Once submitted, your "add forge" request can be in one - of the following states -
-